﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.FtpClient;
using System.IO;
using System.Threading;
using System.Runtime.Remoting.Proxies;
using CloudStorageLight.Core.Web;

namespace CloudStorageLight.Core
{
    public class FtpBlobAdapter : BlobAdapterBase
    {
        class RetryableFtpClient : MarshalByRefObject, IAutoRetry, IDisposable
        {
            string ftpUrl;
            string ftpUser;
            string ftpPassword;
            bool ftps;

            internal FtpClient clientInstance
            {
                get
                {
                    if (!_clientInstance.ContainsKey(ftpUrl))
                    {
                        var instance = new FtpClient();
                        instance.Host = ftpUrl;

                        instance.Credentials = new System.Net.NetworkCredential(ftpUser, ftpPassword);
                        instance.EncryptionMode = ftps ? FtpEncryptionMode.Explicit : FtpEncryptionMode.None;
                        instance.ValidateCertificate += client_ValidateCertificate;

                        _clientInstance[ftpUrl] = instance;
                    }
                    return _clientInstance[ftpUrl];
                }
            }

            //シングルトン対応は見送り
            //private static Dictionary<string,FtpClient> _clientInstance = new Dictionary<string,FtpClient>();
            private Dictionary<string, FtpClient> _clientInstance = new Dictionary<string, FtpClient>();

            public RetryableFtpClient(string ftpUrl, string user, string password, bool ftps = false)
            {
                this.ftpUrl = ftpUrl;
                this.ftpUser = user;
                this.ftpPassword = password;
                this.ftps = ftps;

            }
            void client_ValidateCertificate(FtpClient control, FtpSslValidationEventArgs e)
            {
                e.Accept = true;
            }

            internal FtpListItem[] GetListing(string path, FtpListOption option)
            {
                try
                {
                    return clientInstance.GetListing(path, option);
                }
                catch (FtpCommandException fex)
                {
                    if (fex.CompletionCode == "530") throw new BlobException(BlobExceptionCode.InvalidCustomCredentials);
                    if (fex.CompletionCode == "550") throw new BlobException(BlobExceptionCode.AccessDenied);
                    throw;
                }
            }


            internal bool DirectoryExists(string path)
            {
                var ret = clientInstance.DirectoryExists(path);
                try
                {
                    if (!ret)
                    {
                        //ACCESS DENIED CHECK
                        clientInstance.GetListing(path);
                    }
                    return ret;
                }
                catch (FtpCommandException fex)
                {
                    if (fex.CompletionCode == "530") throw new BlobException(BlobExceptionCode.InvalidCustomCredentials);

                    //IIS用の検知メカニズム この例外は親切なメッセージをだすための仕組み
                    if (fex.CompletionCode == "550" 
                        && fex.Message.IndexOf("access", StringComparison.InvariantCultureIgnoreCase) >= 0
                        && fex.Message.IndexOf("denied", StringComparison.InvariantCultureIgnoreCase) >= 0) throw new BlobException(BlobExceptionCode.AccessDenied);
                    return ret;
                }
            }

            internal bool FileExists(string path, FtpListOption option)
            {
                try
                {
                    return clientInstance.FileExists(path, option);
                }
                catch (FtpCommandException fex)
                {
                    if (fex.CompletionCode == "530") throw new BlobException(BlobExceptionCode.InvalidCustomCredentials);
                    if (fex.CompletionCode == "550") throw new BlobException(BlobExceptionCode.AccessDenied);
                    throw;
                }
            }

            internal void CreateDirectory(string path, bool force)
            {
                try
                {
                    clientInstance.CreateDirectory(path, force);
                }
                catch (FtpCommandException fex)
                {
                    if (fex.CompletionCode == "530") throw new BlobException(BlobExceptionCode.InvalidCustomCredentials);
                    if (fex.CompletionCode == "550") throw new BlobException(BlobExceptionCode.AccessDenied);
                    throw;
                }
            }

            internal Stream OpenRead(string path)
            {
                try
                {
                    return clientInstance.OpenRead(path);
                }
                catch (FtpCommandException fex)
                {
                    if (fex.CompletionCode == "530") throw new BlobException(BlobExceptionCode.InvalidCustomCredentials);
                    if (fex.CompletionCode == "550") throw new BlobException(BlobExceptionCode.AccessDenied);
                    throw;
                }
            }

            internal Stream OpenWrite(string path)
            {
                try
                {
                    return clientInstance.OpenWrite(path);
                }
                catch (FtpCommandException fex)
                {
                    if (fex.CompletionCode == "530") throw new BlobException(BlobExceptionCode.InvalidCustomCredentials);
                    if (fex.CompletionCode == "550") throw new BlobException(BlobExceptionCode.AccessDenied);
                    throw;
                }
            }

            internal void DeleteDirectory(string path, bool force, FtpListOption option)
            {
                try
                {
                    clientInstance.DeleteDirectory(path, force, option);
                }
                catch (FtpCommandException fex)
                {
                    if (fex.CompletionCode == "530") throw new BlobException(BlobExceptionCode.InvalidCustomCredentials);
                    if (fex.CompletionCode == "550") throw new BlobException(BlobExceptionCode.AccessDenied);
                    throw;
                }
            }

            internal void DeleteFile(string path)
            {
                try
                {
                    clientInstance.DeleteFile(path);
                }
                catch (FtpCommandException fex)
                {
                    if (fex.CompletionCode == "530") throw new BlobException(BlobExceptionCode.InvalidCustomCredentials);
                    if (fex.CompletionCode == "550") throw new BlobException(BlobExceptionCode.AccessDenied);
                    throw;
                }
            }

            //internal DateTime GetModifiedTime(string path)
            //{
            //    return clientInstance.GetModifiedTime(path);
            //}

            //internal long GetFileSize(string path)
            //{
            //    return clientInstance.GetFileSize(path);
            //}

            /// <summary>
            /// 後処理
            /// </summary>
            public virtual void Dispose()
            {
                //if (this._clientInstance != null)
                //{
                //    this._clientInstance.Dispose();
                //    this._clientInstance = null;
                //}
            }

            bool IAutoRetry.CatchError(System.Reflection.MethodBase method, Exception ex, int count)
            {
                var ftpex = ex as BlobException;
                if (ftpex != null) return false;
                if (count > 3) return false;
                if (_clientInstance[ftpUrl] != null && count == 2)
                {
                    _clientInstance[ftpUrl].Dispose();
                    _clientInstance.Remove(ftpUrl);
                }
                Thread.Sleep(1000);
                return true;
            }

        }

        string ftpUrl;
        string ftpUser;
        string ftpPassword;
        string authMode;
        bool ftps;
        internal string rootFtpFilePath;

        public FtpBlobAdapter(string rootPath, string ftpUrl, string user, string password, string targetFilePath, bool ftps = false, string timezone = null)
        {
            this.RootPath = rootPath;
            this.ftpUrl = ftpUrl;
            this.ftpUser = user;
            this.ftpPassword = password;
            this.ftps = ftps;
            this.rootFtpFilePath = targetFilePath.TrimEnd('/') + "/";
            this.TimeZone = timezone;
        }

        public FtpBlobAdapter(string rootPath, string ftpUrl, string user, string password, string targetFilePath, bool ftps, string timezone, string authMode)
            :this(rootPath, ftpUrl, user, password, targetFilePath, ftps, timezone)
        {
            this.authMode = authMode;
        }

        public string ConvertFtpPathToUrlPath(string ftpPath)
        {
            if (rootFtpFilePath.Length > ftpPath.Length) return RootPath;
            var uriPath = ftpPath.Substring(rootFtpFilePath.Length);
            uriPath = RootPath + uriPath;
            return uriPath;
        }

        public string ConvertUrlPathToFtpPath(string urlPath)
        {
            return this.rootFtpFilePath + urlPath.Substring(RootPath.Length);
        }



        private RetryableFtpClient GetFtpClient()
        {
            if (clientInstance == null)
            {
                var customAuth = GetCustomAuth(RootPath);
                if (customAuth != null)
                {
                    this.ftpUser = customAuth.User;
                    this.ftpPassword = customAuth.Password;
                }

                var targetObj=  new RetryableFtpClient(this.ftpUrl, this.ftpUser, this.ftpPassword, this.ftps);
                clientInstance =(RetryableFtpClient)(new RetryProxy(targetObj).GetTransparentProxy());
            }
            return clientInstance;
        }
        private RetryableFtpClient clientInstance;

        void client_ValidateCertificate(FtpClient control, FtpSslValidationEventArgs e)
        {
            e.Accept = true;
        }

        /// <summary>
        /// 指定したファイルが存在するか
        /// </summary>
        /// <param name="path"></param>
        /// <returns></returns>
        #pragma warning disable 1998
        protected override async Task<bool> ExistsAsyncInternal(string path)
        {
            
            var client = GetFtpClient();
            var ftpPath = ConvertUrlPathToFtpPath(path);

            if (path.EndsWith("/"))
            {
                return client.DirectoryExists(ftpPath);
            }
            else
            {
                var parent = GetParentFolder(ftpPath);
                if (!client.DirectoryExists(parent)) return false;
                var work = new List<BlobItem>();
                ListFiles(parent, 0, client, work, false);
                return work.Any(x => x.Path == path);
            }
        }
        #pragma warning restore 1998

        string GetParentFolder(string path)
        {
            return path.Substring(0, path.Length - path.Split('/').Last().Length);
        }

        /// <summary>
        /// Blobのコピー
        /// </summary>
        /// <param name="path"></param>
        /// <param name="fromUri"></param>
        /// <returns></returns>
        public override async Task CopyAsync(string path, BlobFile file)
        {
            var client = GetFtpClient();
            var ftpPathDest = ConvertUrlPathToFtpPath(path);
            var ftpPathSrc = ConvertUrlPathToFtpPath(file.Path);
            var ftpPathDire = GetParentFolder(ftpPathDest);
            if (!client.DirectoryExists(ftpPathDire)) client.CreateDirectory(ftpPathDire, true);

            var tmp = Path.GetTempFileName();
            try
            {
                using (var st = new FileStream(tmp, FileMode.Create))
                {
                    using (Stream istream = client.OpenRead(ftpPathSrc))
                    {
                        await istream.CopyToAsync(st);
                    }
                }
                using (Stream ostream = client.OpenWrite(ftpPathDest))
                {
                    using (var st = new FileStream(tmp, FileMode.Open))
                    {
                        await st.CopyToAsync(ostream);
                    }
                }
            }
            finally
            {
                File.Delete(tmp);
            }
           
        }

        /// <summary>
        /// 削除
        /// </summary>
        /// <returns></returns>
        #pragma warning disable 1998
        public override async Task<bool> DeleteAsync(BlobItem file)
        {

            var client = GetFtpClient();
            var ftpFilePath = ConvertUrlPathToFtpPath(file.Path);
            if (file is BlobDirectory)
            {
                if (!client.DirectoryExists(ftpFilePath)) return false;
                client.DeleteDirectory(ftpFilePath, false, FtpListOption.ForceList);

            }
            else
            {
                if (!client.FileExists(ftpFilePath, FtpListOption.ForceList)) return false;
                client.DeleteFile(ftpFilePath);
            }

            ClearExistsCahce();
            return true;
        }
        #pragma warning restore 1998

        /// <summary>
        /// ディレクトリののファイル一覧を取得する
        /// </summary>
        /// <param name="id"></param>
        /// <param name="maxSize"></param>
        /// <returns></returns>
        #pragma warning disable 1998
        public override async Task<List<BlobItem>> GetFilesAsync(string id, int maxSize, bool flat = false)
        {
            var client = GetFtpClient();
            var result = new List<BlobItem>();
            var ftpFilePath = ConvertUrlPathToFtpPath(id.StartsWith(RootPath) ? id : RootPath);

            ListFiles(ftpFilePath, maxSize, client, result, flat);
            return result;
        }
        #pragma warning restore 1998

        protected override void ClearExistsCahce()
        {
            cachedListResult.Clear();
            base.ClearExistsCahce();
        }
        private Dictionary<string, List<BlobItem>>  cachedListResult = new Dictionary<string, List<BlobItem>>();

        private void ListFiles(string id, int maxSize, RetryableFtpClient client, List<BlobItem> result, bool flat)
        {
            var folders = new List<BlobDirectory>();
            var work = new List<BlobItem>();

            if (!cachedListResult.ContainsKey(id))
            {
                foreach (var item in client.GetListing(id, FtpListOption.SizeModify | FtpListOption.ForceList))
                {
                    var logicalPath = ConvertFtpPathToUrlPath(item.FullName);
                    BlobItem fileItem = null;
                    switch (item.Type)
                    {
                        case FtpFileSystemObjectType.Directory:
                            var folder = new BlobDirectory(Parent, logicalPath);
                            folders.Add(folder);
                            fileItem = folder;
                            break;
                        case FtpFileSystemObjectType.File:
                            fileItem = new BlobFile(Parent, logicalPath);
                            fileItem.Length = item.Size;
                            var modified = DateTime.SpecifyKind(item.Modified, DateTimeKind.Utc);
                            fileItem.LastModified = ConvertTimezone(modified);
                            break;
                    }
                    if (fileItem != null) work.Add(fileItem);
                }
                cachedListResult[id] = work;
            }
            else
            {
                foreach (var item in cachedListResult[id])
                {
                    var dire = item as BlobDirectory;
                    if (dire != null)
                    {
                        folders.Add(dire);
                    }
                    else
                    {
                        work.Add(item);
                    }
                }
            }

            foreach (var fileItem in work.OrderBy(x=>x.Path))
            {
                result.Add(fileItem);
                if (maxSize > 0 && result.Count >= maxSize) break;
            }

            if (flat)
            {
                foreach (var folder in folders)
                {
                    ListFiles(ConvertUrlPathToFtpPath(folder.Path), maxSize, client, result, flat);
                    if (maxSize > 0 && result.Count >= maxSize)
                    {
                        break;
                    }
                }
            }
        }


        /// <summary>
        /// アップロード
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public override async Task UploadAsync(BlobItem item, Stream stream)
        {
            var client = GetFtpClient();
            var ftpPath = ConvertUrlPathToFtpPath(item.Path);
            using (Stream ostream = client.OpenWrite(ftpPath))
            {
                await stream.CopyToAsync(ostream);
            }
            ClearExistsCahce();
        }

        /// <summary>
        /// アップロード
        /// </summary>
        /// <param name="localpath"></param>
        /// <returns></returns>
        public override async Task UploadAsync(BlobItem item, string localpath)
        {
            using (var st = new FileStream(localpath, FileMode.Open))
            {
                await UploadAsync(item, st);
            }
        }

        /// <summary>
        /// ダウンロード
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public override async Task DownloadAsync(BlobItem item, Stream stream)
        {
            var client = GetFtpClient();
            var ftpPath = ConvertUrlPathToFtpPath(item.Path);
            using (Stream istream = client.OpenRead(ftpPath))
            {
                await istream.CopyToAsync(stream);
            }
        }

        /// <summary>
        /// ダウンロード
        /// </summary>
        /// <param name="localpath"></param>
        /// <returns></returns>
        public override async Task DownloadAsync(BlobItem item, string localpath)
        {
            using (var st = new FileStream(localpath, FileMode.Create))
            {
                await DownloadAsync(item, st);
            }
        }

        /// <summary>
        /// フォルダ作成
        /// </summary>
        /// <param name="dire"></param>
        /// <returns></returns>
        #pragma warning disable 1998
        public override async Task CreateDirectoryAsync(BlobDirectory dire)
        {
            var client = GetFtpClient();
            var ftpPath = ConvertUrlPathToFtpPath(dire.Path);
            client.CreateDirectory(ftpPath, true);
            ClearExistsCahce();
        }
        #pragma warning restore 1998
        /// <summary>
        /// メタ情報の設定
        /// </summary>
        /// <returns></returns>
        #pragma warning disable 1998
        public override async Task SetMetadataAsync(BlobItem item)
        {
            item.Link = ConvertUrlPathToFtpPath(item.Path);

        }
        #pragma warning restore 1998

        /// <summary>
        /// メタ情報の取得
        /// </summary>
        /// <param name="fetch"></param>
        /// <returns></returns>
        public override async Task GetMetadataAsync(BlobItem file)
        {
            var client = GetFtpClient();

            if (file.Path.EndsWith("/"))
            {
                //var ftpPath = ConvertUrlPathToFtpPath(file.Path);
                //if(client.DirectoryExists(ftpPath))
                //{
                //    //チューニング目的でディレクトリの場合にはとらない
                //    //var modified = DateTime.SpecifyKind(client.GetModifiedTime(ftpPath), DateTimeKind.Utc);
                //    //file.LastModified = modified;
                //    //file.Length = client.GetFileSize(ftpPath);
                //}
            }
            else
            {
                var parent = GetParentFolder(file.Path);
                if (await ExistsAsync(parent))
                {
                    var work = new List<BlobItem>();
                    ListFiles(ConvertUrlPathToFtpPath(parent), 0, client, work, false);
                    var fileItem = work.Where(x => x.Path == file.Path).FirstOrDefault();
                    if (fileItem != null)
                    {
                        file.LastModified = fileItem.LastModified;
                        file.Length = fileItem.Length;                        
                    }
                }
            }
        }

        public override CustomAuthInfo GetCustomAuth(string path)
        {
            if (!string.IsNullOrEmpty(this.authMode) && this.authMode != "config")
            {
                var customAuth = base.GetCustomAuth(rootFtpFilePath);
                if (customAuth != null) return customAuth;
                return new CustomAuthInfo { Key = RootPath, Name = this.ftpUrl, Type = this.GetType().FullName, Path = this.RootPath, AuthMode = this.authMode };
            }
            return null;
        }

        /// <summary>
        /// 後処理
        /// </summary>
        public override void Dispose()
        {
            if (this.clientInstance != null)
            {
                this.clientInstance.Dispose();
                this.clientInstance = null;
            }
        }

    }

}
